// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ProxyDefinition.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization;

    /// <summary>
    /// A proxy identifies an agent exposed on the network (either local/near or remote/far). 
    /// Remote agents can be subscribed to and can be controlled via control messages.
    /// </summary>
    [DataContract(Name = "Proxy")]
    public class ProxyDefinition
    {
        /// <summary>
        /// The name of the proxy agent.
        /// </summary>
        [DataMember(Name = "Name")]
        private readonly string name;

        /// <summary>
        /// The name of the agent this proxy subscribes to (i.e. the agent to network-enable or the agent to record).
        /// </summary>
        [DataMember(Name = "Producer")]
        private readonly string producer;

        /// <summary>
        /// The publishing contracts exposed by the agent.
        /// </summary>
        [DataMember(Name = "Publish")]
        private readonly PortSetDefinition publishSet;

        /// <summary>
        /// The control contracts exposed by the agent.
        /// </summary>
        [DataMember(Name = "Control")]
        private readonly PortSetDefinition controlSet;

        /// <summary>
        /// The name or ip address of the machine where the remote agent is running.
        /// </summary>
        [DataMember(Name = "NetworkAddress")]
        private readonly string networkAddress;

        /// <summary>
        /// Initializes a new instance of the ProxyDefinition class.
        /// This constructor is never called when deserializing from the manifest, and is primarily intended for tests.
        /// </summary>
        /// <param name="name">The name of the proxy.</param>
        /// <param name="producer">The name of the agent generating the messages to record or broadcast.</param>
        /// <param name="address">The remote name or address.</param>
        /// <param name="publishContracts">The types of messages to replay/record/broadcast.</param>
        /// <param name="publishNetworkPortNumber">The port for published messages.</param>
        /// <param name="controlContracts">The types of control messages to enable.</param>
        /// <param name="controlNetworkPortNumber">The port for control messages.</param>
        internal ProxyDefinition(
            string name,
            string producer,
            string address, 
            Type[] publishContracts, 
            int publishNetworkPortNumber,
            Type[] controlContracts, 
            int controlNetworkPortNumber)
        {
            this.networkAddress = address;
            this.name = name;
            this.producer = producer;
            this.publishSet = publishContracts == null ? null : new PortSetDefinition(publishContracts, publishNetworkPortNumber);
            this.controlSet = controlContracts == null ? null : new PortSetDefinition(controlContracts, controlNetworkPortNumber);
        }

        /// <summary>
        /// Gets the name of the (local or remote) agent.
        /// </summary>
        public string Name
        {
            get
            {
                return this.name;
            }
        }

        /// <summary>
        /// Gets the file path of the file proxy agent.
        /// </summary>
        public string Producer
        {
            get
            {
                return this.producer;
            }
        }

        /// <summary>
        /// Gets the publishing contracts exposed by the agent.
        /// </summary>
        public PortSetDefinition PublishSet
        {
            get
            {
                return this.publishSet;
            }
        }

        /// <summary>
        /// Gets the control contracts exposed by the agent.
        /// </summary>
        public PortSetDefinition ControlSet
        {
            get
            {
                return this.controlSet;
            }
        }

        /// <summary>
        /// Gets the name or ip address of the machine where the remote agent is running.
        /// </summary>
        public string NetworkAddress
        {
            get
            {
                return this.networkAddress;
            }
        }

        /// <summary>
        /// Creates a proxy that broadcasts over the network the agents published by the specified producer agent.
        /// This is intended for tests and manifest creation only.
        /// </summary>
        /// <param name="name">The name of the agent.</param>
        /// <param name="producer">The name of the agent generating the messages to record or broadcast.</param>
        /// <param name="address">The remote name or address.</param>
        /// <param name="publishContracts">The publishing message types to enable for remoting.</param>
        /// <param name="publishNetworkPortNumber">The port for published messages.</param>
        /// <param name="controlContracts">The control message types to enable for remoting.</param>
        /// <param name="controlNetworkPortNumber">The port for control messages.</param>
        /// <returns>A ProxyDefinition instance representing a network enabled agent proxy</returns>
        public static ProxyDefinition CreateNetworkEnabledProxy(
            string name,
            string producer,
            string address,
            Type[] publishContracts,
            int publishNetworkPortNumber,
            Type[] controlContracts,
            int controlNetworkPortNumber)
        {
            return new ProxyDefinition(name, producer, address, publishContracts, publishNetworkPortNumber, controlContracts, controlNetworkPortNumber);
        }

        /// <summary>
        /// Creates a proxy that listens to the specified network port for messages and impersonates the remote agent that publishes them.
        /// This is intended for tests and manifest creation only.
        /// </summary>
        /// <param name="name">The name of the agent.</param>
        /// <param name="address">The remote name or address.</param>
        /// <param name="publishContracts">The publishing message types to enable for remoting.</param>
        /// <param name="publishNetworkPortNumber">The port for published messages.</param>
        /// <param name="controlContracts">The control message types to enable for remoting.</param>
        /// <param name="controlNetworkPortNumber">The port for control messages.</param>
        /// <returns>A ProxyDefinition instance representing a remote agent proxy</returns>
        public static ProxyDefinition CreateRemoteProxy(
            string name,
            string address,
            Type[] publishContracts,
            int publishNetworkPortNumber,
            Type[] controlContracts,
            int controlNetworkPortNumber)
        {
            return new ProxyDefinition(name, null, address, publishContracts, publishNetworkPortNumber, controlContracts, controlNetworkPortNumber);
        }

        /// <summary>
        /// Returns the set of types needed to serialize this proxy. 
        /// </summary>
        /// <returns>The set of types needed to serialize this proxy</returns>
        public IEnumerable<Type> GetKnownTypes()
        {
            List<Type> types = new List<Type>();
            if (this.ControlSet != null)
            {
                types.AddRange(this.ControlSet.GetContracts());
            }

            if (this.PublishSet != null)
            {
                types.AddRange(this.PublishSet.GetContracts());
            }

            types.Add(typeof(Type).GetType());

            return types;
        }

        /// <summary>
        /// Returns an IProducer if the proxy publishes the provided type.
        /// Useful in maintaining type safety when creating a manifest.
        /// </summary>
        /// <typeparam name="T">The type of message</typeparam>
        /// <returns>An IProducer for the specified message type.</returns>
        public IProducer<T> AsProducer<T>()
            where T : AgentMessage
        {
            if (!Array.Exists(this.publishSet.GetContracts(), p => p == typeof(T)))
            {
                throw new InvalidOperationException(string.Format("This proxy does not publish messages of type {0}", typeof(T).Name));
            }

            ProxyProducer<T> producer = new ProxyProducer<T>();
            producer.Name = this.name;
            return producer;
        }

        /// <summary>
        /// Returns an IControllable if the proxy accepts the provided type as control message.
        /// Useful in maintaining type safety when creating a manifest.
        /// </summary>
        /// <typeparam name="T">The type of message</typeparam>
        /// <returns>An IControllable for the specified message type.</returns>
        public IControllable<T> AsControllable<T>()
            where T : AgentMessage
        {
            if (!Array.Exists(this.controlSet.GetContracts(), p => p == typeof(T)))
            {
                throw new InvalidOperationException(string.Format("This proxy does not publish messages of type {0}", typeof(T).Name));
            }

            ProxyControllable<T> producer = new ProxyControllable<T>();
            producer.Name = this.name;
            return producer;
        }

        /// <summary>
        /// A trivial implementation of IProducer that makes it possible to create manifests with proxies in them and maintain some level of type safety.
        /// </summary>
        /// <typeparam name="T">The type of message</typeparam>
        public class ProxyProducer<T> : IProducer<T>
            where T : AgentMessage
        {
            /// <summary>
            /// Gets or sets the name of the proxy
            /// </summary>
            public string Name { get; set; }
        }

        /// <summary>
        /// A trivial implementation of IControllable that makes it possible to create manifests with proxies in them and maintain some level of type safety.
        /// </summary>
        /// <typeparam name="T">The type of message</typeparam>
        public class ProxyControllable<T> : IControllable<T>
            where T : AgentMessage
        {
            /// <summary>
            /// Gets or sets the name of the proxy
            /// </summary>
            public string Name { get; set; }
        }

        /// <summary>
        /// Groups together the exposed pins and the associated port.
        /// </summary>
        [DataContract]
        public class PortSetDefinition
        {
            /// <summary>
            /// The port that these pins either listen to or send to.
            /// </summary>
            [DataMember(Name = "NetworkPort")]
            private readonly int networkPortNumber;

            /// <summary>
            /// The contracts exposed by the agent.
            /// </summary>
            [DataMember(Name = "AgentMessages")]
            private string[] messageContracts;

            /// <summary>
            /// Initializes a new instance of the PortSetDefinition class.
            /// </summary>
            /// <param name="messageContracts">The kinds of messages to expose. They uniquely identify the ports to enable.</param>
            /// <param name="networkPortNumber">The network port associated with the agent port.</param>
            public PortSetDefinition(Type[] messageContracts, int networkPortNumber)
            {
                this.messageContracts = messageContracts.Select(m => m.AssemblyQualifiedName).ToArray();
                this.networkPortNumber = networkPortNumber;
            }

            /// <summary>
            /// Gets the port that these pins either listen to or send to.
            /// </summary>
            public int Port
            {
                get
                {
                    return this.networkPortNumber;
                }
            }

            /// <summary>
            /// Gets the contracts exposed by the agent.
            /// </summary>
            /// <returns>An array of types</returns>
            public Type[] GetContracts()
            {
                return this.messageContracts.Select(m => Type.GetType(m)).ToArray();
            }
        }
    }
}
